<?php

/**
 * Module bootstrap base class.
 *
 * A module bootstrap executes any additional functions that are needed by the
 * module actions to work properly.
 *
 * A module bootstrap is also made available to any controller and view via the
 * Mz_Module_Helper_Module action/view helper (registered automatically by the
 * Mz_Module_Initializer plugin), so subclasses of this abstract can also
 * contain methods that relate to the module which you might want to invoke
 * elsewhere. E.g., a Blog module bootstrap might contain a method that
 * returns a list of recent blog entries.
 *
 * @author    Mon Zafra <monzee at gmail>
 * @copyright (c)2009 Mon Zafra
 * @category  Mz
 * @package   Mz_Module
 * @license   http://mz-project.googlecode.com/svn/trunk/LICENSE     MIT License
 * @version   SVN: $Id: Bootstrap.php 14 2009-03-04 10:58:54Z monzee $
 */
abstract class Mz_Module_Bootstrap
{
    /**
     * Prefix of module-specific helpers and other classes.
     * Typically the module name itself
     *
     * @var string
     */
    protected $_prefix;

    /**
     * Module path.
     *
     * @var string
     */
    protected $_path;

    /**
     * Spec of the generated links to internal module resources.
     *
     * @var string
     */
    protected $_resourceUrlSpec = ':base/:resmodule/:prefix/:resource';

    /**
     * Has the preRoute() method been called already?
     *
     * @var bool
     */
    static protected $_preRouted = false;

    /**
     * Front controller.
     *
     * @var Zend_Controller_Front
     */
    protected $_front;

    /**
     * Registers autoloader, calls preRoute() and init().
     *
     * @param string $module Module name
     * @param string $path   Module path
     */
    public function __construct($module, $path)
    {
        $module = ucfirst(strtolower($module));
        $this->_prefix = rtrim($module, '_');

        $this->_path = $path;

        spl_autoload_register(array($this, 'load'));

        if (!self::$_preRouted) {
            $this->preRoute($module, $path);
        }
        $this->init();
        
        // because all subclasses of this abstract uses this flag.
        // static madness. 5.3 woud address this.
        self::$_preRouted = false;
    }

    /**
     * Returns the front controller instance.
     * 
     * @return Zend_Controller_Front
     */
    public function getFrontController()
    {
        if (null === $this->_front) {
            $this->_front = Zend_Controller_Front::getInstance();
        }

        return $this->_front;
    }

    /**
     * Registers plugin if it hasn't been registered with the front controller.
     *
     * @param string $pluginName Plugin class name
     * @param int    $stack      stack index
     * @return Mz_Module_Bootstrap
     */
    protected function _registerIfNone($pluginName, $stack = null)
    {
        $front = $this->getFrontController();
        if (!$front->hasPlugin($pluginName)) {
            $plugin = new $pluginName();
            $front->registerPlugin($plugin, $stack);
        }

        return $this;
    }

    /**
     * Aggressive module initialization.
     * 
     * This method is called (if registered) upon the instantiation of the module
     * initializer regardless of whether or not the request maps to this module.
     * This is essentially an extension of the application bootstrap.
     * 
     * This hook would typically contain functions that you want to execute
     * regardless of the module dispatched. For example, if your module needs to
     * have a certain routing rule set to function properly, they should be put 
     * in preRoute() and not in init(). If the routes were set in init(), it
     * would have been too late to take effect since init() is called during
     * dispatchLoopStartup() (if not manually instantiated elsewhere).
     *
     * Any module which implements this must be registered to the module
     * initializer via the constructor option (preRouteHooks array) or
     * Mz_Module_Initializer::addPreRouteHook(). For the latter, make sure that
     * the addPreRouteHook() is called _before_ the module initializer plugin is
     * instantiated.
     *
     * @param string $module Module name
     * @param string $path   Module path
     */
    static public function preRoute($module, $path) { }

    /**
     * Lazy module initialization.
     *
     * This is also essentially an extension of the application bootstrap but
     * unlike preRoute(), this method is only executed if the request maps to
     * this module, or if a module bootstrap method is specifically invoked
     * in a controller action via $this->_helper->module($module).
     * 
     * Register plugins and helpers needed by the module controllers here and/or
     * add internal module paths to the include path. This is always executed
     * after instantiation of the bootstrap.
     *
     * The preRoute() method is also always called during instantiation, so any
     * object state changes made in the preRoute() also applies here. I.e., if
     * you set a static member in preRoute(), you can also use it in this
     * method.
     */
    public function init() { }

    /**
     * Sets the prefix of module-specific classes.
     *
     * @param string $prefix
     */
    public function setPrefix($prefix)
    {
        $this->_prefix = $prefix;
    }

    /**
     * Sets the module path. Not sure how useful this is.
     *
     * @param string $path
     */
    public function setPath($path)
    {
        $this->_path = $path;
    }

    /**
     * Sets the resource url pattern.
     *
     * @param string $spec 
     */
    public function setResourceUrlSpec($spec)
    {
        $this->_resourceUrlSpec = $spec;
    }

    /**
     * Sets the subpath in the public directory where this module's resources
     * map to.
     * 
     * @param string $path 
     */
    public function setPublicSubpath($path)
    {
        $path = trim($path, '/');
        $this->_resourceUrlSpec = ':base/' . $path . '/:resource';
    }

    /**
     * Sets the prerouted state.
     *
     * If this is true during instantiation of the bootstrap, the preRoute()
     * hook won't be called.
     * 
     * @param bool $state 
     */
    static public function setPreRouted($state = true)
    {
        self::$_preRouted = $state;
    }

    /**
     * Loads files inside the module directory.
     *
     * Registered as an autoloader automatically when instantiated. This method
     * should map classes which cannot be loaded by the main app to files inside
     * this module directory.
     *
     * @param string $class Class name
     * @return bool Was the class file found?
     * @todo I think autoloaders should return the name of the class if the
     *       method fails to load the class. Need to confirm.
     */
    public function load($class)
    {
        $prefix = $this->_prefix;

        if ($prefix == substr($class, 0, strlen($prefix))) {
            $unprefixed = substr($class, strlen($prefix));
            $trans = str_replace('_', DIRECTORY_SEPARATOR, $unprefixed) . '.php';

            $file = $this->pathTo('resources' . DIRECTORY_SEPARATOR . $trans);
            if (file_exists($file)) {
                include_once $file;
                return true;
            }

            $file = $this->pathTo('models' . DIRECTORY_SEPARATOR . $trans);
            if (file_exists($file)) {
                include_once $file;
                return true;
            }
        }

        $trans = str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
        $file = $this->pathTo('library' . DIRECTORY_SEPARATOR . $trans);
        if (file_exists($file)) {
            include_once $file;
            return true;
        }

        return false;
    }

    /**
     * Returns the path relative to module base dir.
     *
     * This shouldn't be used in the preRoute() method because the $_path property
     * would still be blank by then. 
     *
     * @param string $path String to append to base module path
     * @return string
     * @todo Use 5.3 features to autodiscover the path.
     */
    public function pathTo($path = null)
    {
        $ret = $this->_path;

        if (!empty($path)) {
            $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, trim($path, '/\\'));
            $ret .= DIRECTORY_SEPARATOR . $path;
        }

        return $ret;
    }

    /**
     * Generates a link to an internal module resource.
     * 
     * Recommended project structure: ('foo' is the module name)
     *
     * project
     * + application
     * | +-modules
     * |   +-foo
     * |     +-public
     * |       +-resource.ext
     * +-public
     *   +-resource
     *     +-foo
     *       +-resource.ext
     *
     * The resource mapper plugin/module can automatically fetch any file
     * in project/application/modules/foo/public when invoked with the link
     * http://host/<base_uri>/resource/foo/<filename>. If there already exists a
     * module by the name of resource, _resource is used as the module name.
     * 
     * @param string $resource
     * @param string $baseUrl
     * @param string $prefix
     * @return string
     * @todo Autofill $baseUrl if null?
     * @todo 'modules' instead of 'resource' might be a better semantically?
     */
    public function urlToResource($resource = null, $baseUrl = null, $prefix = null)
    {
        $spec = $this->_resourceUrlSpec;
        $prefix = null === $prefix ? strtolower($this->_prefix) : $prefix;
        $modRes = Zend_Controller_Front::getInstance()->getPlugin('Mz_Module_Resource');

        $replaceMap = array(
            ':resmodule' => $modRes->getResourcesModule(),
            ':base' => $baseUrl,
            ':prefix' => $prefix,
            ':resource' => $resource,
        );

        return str_replace(array_keys($replaceMap), array_values($replaceMap), $spec);
    }

}
